//===-- OperatingSystemGo.cpp -----------------------------------*- C++ -*-===//
//
//                     The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//

// C Includes
// C++ Includes
#include <unordered_map>

// Other libraries and framework includes
// Project includes
#include "OperatingSystemGo.h"

#include "lldb/Core/DataBufferHeap.h"
#include "lldb/Core/Debugger.h"
#include "lldb/Core/Module.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Core/RegisterValue.h"
#include "lldb/Core/Section.h"
#include "lldb/Core/StreamString.h"
#include "lldb/Core/ValueObjectVariable.h"
#include "lldb/Interpreter/CommandInterpreter.h"
#include "lldb/Interpreter/OptionValueProperties.h"
#include "lldb/Interpreter/Options.h"
#include "lldb/Interpreter/OptionGroupBoolean.h"
#include "lldb/Interpreter/OptionGroupUInt64.h"
#include "lldb/Interpreter/Property.h"
#include "lldb/Symbol/ObjectFile.h"
#include "lldb/Symbol/Type.h"
#include "lldb/Symbol/VariableList.h"
#include "lldb/Target/Process.h"
#include "lldb/Target/StopInfo.h"
#include "lldb/Target/Target.h"
#include "lldb/Target/ThreadList.h"
#include "lldb/Target/Thread.h"
#include "Plugins/Process/Utility/DynamicRegisterInfo.h"
#include "Plugins/Process/Utility/RegisterContextMemory.h"
#include "Plugins/Process/Utility/ThreadMemory.h"

using namespace lldb;
using namespace lldb_private;

namespace
{

static PropertyDefinition g_properties[] = {{"enable", OptionValue::eTypeBoolean, true, true, nullptr, nullptr,
                                             "Specify whether goroutines should be treated as threads."},
                                            {NULL, OptionValue::eTypeInvalid, false, 0, NULL, NULL, NULL}};

enum
{
    ePropertyEnableGoroutines,
};

class PluginProperties : public Properties
{
public:
    PluginProperties()
        : Properties()
    {
        m_collection_sp.reset(new OptionValueProperties(GetSettingName()));
        m_collection_sp->Initialize(g_properties);
    }

    ~PluginProperties() override = default;

    static ConstString
    GetSettingName()
    {
        return OperatingSystemGo::GetPluginNameStatic();
    }

    bool
    GetEnableGoroutines()
    {
        const uint32_t idx = ePropertyEnableGoroutines;
        return m_collection_sp->GetPropertyAtIndexAsBoolean(NULL, idx, g_properties[idx].default_uint_value);
    }

    bool
    SetEnableGoroutines(bool enable)
    {
        const uint32_t idx = ePropertyEnableGoroutines;
        return m_collection_sp->SetPropertyAtIndexAsUInt64(NULL, idx, enable);
    }
};

typedef std::shared_ptr<PluginProperties> OperatingSystemGoPropertiesSP;

static const OperatingSystemGoPropertiesSP &
GetGlobalPluginProperties()
{
    static OperatingSystemGoPropertiesSP g_settings_sp;
    if (!g_settings_sp)
        g_settings_sp.reset(new PluginProperties());
    return g_settings_sp;
}

class RegisterContextGo : public RegisterContextMemory
{
public:
    RegisterContextGo(lldb_private::Thread &thread, uint32_t concrete_frame_idx, DynamicRegisterInfo &reg_info,
                      lldb::addr_t reg_data_addr)
        : RegisterContextMemory(thread, concrete_frame_idx, reg_info, reg_data_addr)
    {
        const RegisterInfo *sp = reg_info.GetRegisterInfoAtIndex(
            reg_info.ConvertRegisterKindToRegisterNumber(eRegisterKindGeneric, LLDB_REGNUM_GENERIC_SP));
        const RegisterInfo *pc = reg_info.GetRegisterInfoAtIndex(
            reg_info.ConvertRegisterKindToRegisterNumber(eRegisterKindGeneric, LLDB_REGNUM_GENERIC_PC));
        size_t byte_size = std::max(sp->byte_offset + sp->byte_size, pc->byte_offset + pc->byte_size);

        DataBufferSP reg_data_sp(new DataBufferHeap(byte_size, 0));
        m_reg_data.SetData(reg_data_sp);
    }

    ~RegisterContextGo() override = default;

    bool
    ReadRegister(const lldb_private::RegisterInfo *reg_info,
                 lldb_private::RegisterValue &reg_value) override
    {
        switch (reg_info->kinds[eRegisterKindGeneric])
        {
            case LLDB_REGNUM_GENERIC_SP:
            case LLDB_REGNUM_GENERIC_PC:
                return RegisterContextMemory::ReadRegister(reg_info, reg_value);
            default:
                reg_value.SetValueToInvalid();
                return true;
        }
    }

    bool
    WriteRegister(const lldb_private::RegisterInfo *reg_info,
                  const lldb_private::RegisterValue &reg_value) override
    {
        switch (reg_info->kinds[eRegisterKindGeneric])
        {
            case LLDB_REGNUM_GENERIC_SP:
            case LLDB_REGNUM_GENERIC_PC:
                return RegisterContextMemory::WriteRegister(reg_info, reg_value);
            default:
                return false;
        }
    }

private:
    DISALLOW_COPY_AND_ASSIGN(RegisterContextGo);
};

} // anonymous namespace

struct OperatingSystemGo::Goroutine
{
    uint64_t m_lostack;
    uint64_t m_histack;
    uint64_t m_goid;
    addr_t m_gobuf;
    uint32_t m_status;
};

void
OperatingSystemGo::Initialize()
{
    PluginManager::RegisterPlugin(GetPluginNameStatic(), GetPluginDescriptionStatic(), CreateInstance,
                                  DebuggerInitialize);
}

void
OperatingSystemGo::DebuggerInitialize(Debugger &debugger)
{
    if (!PluginManager::GetSettingForOperatingSystemPlugin(debugger, PluginProperties::GetSettingName()))
    {
        const bool is_global_setting = true;
        PluginManager::CreateSettingForOperatingSystemPlugin(
            debugger, GetGlobalPluginProperties()->GetValueProperties(),
            ConstString("Properties for the goroutine thread plug-in."), is_global_setting);
    }
}

void
OperatingSystemGo::Terminate()
{
    PluginManager::UnregisterPlugin(CreateInstance);
}

OperatingSystem *
OperatingSystemGo::CreateInstance(Process *process, bool force)
{
    if (!force)
    {
        TargetSP target_sp = process->CalculateTarget();
        if (!target_sp)
            return nullptr;
        ModuleList &module_list = target_sp->GetImages();
        Mutex::Locker modules_locker(module_list.GetMutex());
        const size_t num_modules = module_list.GetSize();
        bool found_go_runtime = false;
        for (size_t i = 0; i < num_modules; ++i)
        {
            Module *module = module_list.GetModulePointerAtIndexUnlocked(i);
            const SectionList *section_list = module->GetSectionList();
            if (section_list)
            {
                SectionSP section_sp(section_list->FindSectionByType(eSectionTypeGoSymtab, true));
                if (section_sp)
                {
                    found_go_runtime = true;
                    break;
                }
            }
        }
        if (!found_go_runtime)
            return nullptr;
    }
    return new OperatingSystemGo(process);
}

OperatingSystemGo::OperatingSystemGo(lldb_private::Process *process)
    : OperatingSystem(process)
    , m_reginfo(new DynamicRegisterInfo)
{
}

OperatingSystemGo::~OperatingSystemGo() = default;

ConstString
OperatingSystemGo::GetPluginNameStatic()
{
    static ConstString g_name("goroutines");
    return g_name;
}

const char *
OperatingSystemGo::GetPluginDescriptionStatic()
{
    return "Operating system plug-in that reads runtime data-structures for goroutines.";
}

bool
OperatingSystemGo::Init(ThreadList &threads)
{
    if (threads.GetSize(false) < 1)
        return false;
    TargetSP target_sp = m_process->CalculateTarget();
    if (!target_sp)
        return false;
    m_allg_sp = FindGlobal(target_sp, "runtime.allg");
    m_allglen_sp = FindGlobal(target_sp, "runtime.allglen");

    if (m_allg_sp && !m_allglen_sp)
    {
        StreamSP error_sp = target_sp->GetDebugger().GetAsyncErrorStream();
        error_sp->Printf("Unsupported Go runtime version detected.");
        return false;
    }

    if (!m_allg_sp)
        return false;

    RegisterContextSP real_registers_sp = threads.GetThreadAtIndex(0, false)->GetRegisterContext();

    std::unordered_map<size_t, ConstString> register_sets;
    for (size_t set_idx = 0; set_idx < real_registers_sp->GetRegisterSetCount(); ++set_idx)
    {
        const RegisterSet *set = real_registers_sp->GetRegisterSet(set_idx);
        ConstString name(set->name);
        for (size_t reg_idx = 0; reg_idx < set->num_registers; ++reg_idx)
        {
            register_sets[reg_idx] = name;
        }
    }
    TypeSP gobuf_sp = FindType(target_sp, "runtime.gobuf");
    if (!gobuf_sp)
    {
        Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_OS));

        if (log)
            log->Printf("OperatingSystemGo unable to find struct Gobuf");
        return false;
    }
    CompilerType gobuf_type(gobuf_sp->GetLayoutCompilerType());
    for (size_t idx = 0; idx < real_registers_sp->GetRegisterCount(); ++idx)
    {
        RegisterInfo reg = *real_registers_sp->GetRegisterInfoAtIndex(idx);
        int field_index = -1;
        if (reg.kinds[eRegisterKindGeneric] == LLDB_REGNUM_GENERIC_SP)
        {
            field_index = 0;
        }
        else if (reg.kinds[eRegisterKindGeneric] == LLDB_REGNUM_GENERIC_PC)
        {
            field_index = 1;
        }
        if (field_index == -1)
        {
            reg.byte_offset = ~0;
        }
        else
        {
            std::string field_name;
            uint64_t bit_offset = 0;
            CompilerType field_type =
                gobuf_type.GetFieldAtIndex(field_index, field_name, &bit_offset, nullptr, nullptr);
            reg.byte_size = field_type.GetByteSize(nullptr);
            reg.byte_offset = bit_offset / 8;
        }
        ConstString name(reg.name);
        ConstString alt_name(reg.alt_name);
        m_reginfo->AddRegister(reg, name, alt_name, register_sets[idx]);
    }
    return true;
}

//------------------------------------------------------------------
// PluginInterface protocol
//------------------------------------------------------------------
ConstString
OperatingSystemGo::GetPluginName()
{
    return GetPluginNameStatic();
}

uint32_t
OperatingSystemGo::GetPluginVersion()
{
    return 1;
}

bool
OperatingSystemGo::UpdateThreadList(ThreadList &old_thread_list, ThreadList &real_thread_list,
                                    ThreadList &new_thread_list)
{
    new_thread_list = real_thread_list;
    Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_OS));

    if (!(m_allg_sp || Init(real_thread_list)) || (m_allg_sp && !m_allglen_sp) ||
        !GetGlobalPluginProperties()->GetEnableGoroutines())
    {
        return new_thread_list.GetSize(false) > 0;
    }

    if (log)
        log->Printf("OperatingSystemGo::UpdateThreadList(%d, %d, %d) fetching thread data from Go for pid %" PRIu64,
                    old_thread_list.GetSize(false), real_thread_list.GetSize(false), new_thread_list.GetSize(0),
                    m_process->GetID());
    uint64_t allglen = m_allglen_sp->GetValueAsUnsigned(0);
    if (allglen == 0)
    {
        return new_thread_list.GetSize(false) > 0;
    }
    std::vector<Goroutine> goroutines;
    // The threads that are in "new_thread_list" upon entry are the threads from the
    // lldb_private::Process subclass, no memory threads will be in this list.

    Error err;
    for (uint64_t i = 0; i < allglen; ++i)
    {
        goroutines.push_back(CreateGoroutineAtIndex(i, err));
        if (err.Fail())
        {
            err.PutToLog(log, "OperatingSystemGo::UpdateThreadList");
            return new_thread_list.GetSize(false) > 0;
        }
    }
    // Make a map so we can match goroutines with backing threads.
    std::map<uint64_t, ThreadSP> stack_map;
    for (uint32_t i = 0; i < real_thread_list.GetSize(false); ++i)
    {
        ThreadSP thread = real_thread_list.GetThreadAtIndex(i, false);
        stack_map[thread->GetRegisterContext()->GetSP()] = thread;
    }
    for (const Goroutine &goroutine : goroutines)
    {
        if (0 /* Gidle */ == goroutine.m_status || 6 /* Gdead */ == goroutine.m_status)
        {
            continue;
        }
        ThreadSP memory_thread = old_thread_list.FindThreadByID(goroutine.m_goid, false);
        if (memory_thread && IsOperatingSystemPluginThread(memory_thread) && memory_thread->IsValid())
        {
            memory_thread->ClearBackingThread();
        }
        else
        {
            memory_thread.reset(new ThreadMemory(*m_process, goroutine.m_goid, nullptr, nullptr, goroutine.m_gobuf));
        }
        // Search for the backing thread if the goroutine is running.
        if (2 == (goroutine.m_status & 0xfff))
        {
            auto backing_it = stack_map.lower_bound(goroutine.m_lostack);
            if (backing_it != stack_map.end())
            {
                if (goroutine.m_histack >= backing_it->first)
                {
                    if (log)
                        log->Printf("OperatingSystemGo::UpdateThreadList found backing thread %" PRIx64 " (%" PRIx64
                                    ") for thread %" PRIx64 "",
                                    backing_it->second->GetID(), backing_it->second->GetProtocolID(),
                                    memory_thread->GetID());
                    memory_thread->SetBackingThread(backing_it->second);
                    new_thread_list.RemoveThreadByID(backing_it->second->GetID(), false);
                }
            }
        }
        new_thread_list.AddThread(memory_thread);
    }

    return new_thread_list.GetSize(false) > 0;
}

void
OperatingSystemGo::ThreadWasSelected(Thread *thread)
{
}

RegisterContextSP
OperatingSystemGo::CreateRegisterContextForThread(Thread *thread, addr_t reg_data_addr)
{
    RegisterContextSP reg_ctx_sp;
    if (!thread)
        return reg_ctx_sp;

    if (!IsOperatingSystemPluginThread(thread->shared_from_this()))
        return reg_ctx_sp;

    reg_ctx_sp.reset(new RegisterContextGo(*thread, 0, *m_reginfo, reg_data_addr));
    return reg_ctx_sp;
}

StopInfoSP
OperatingSystemGo::CreateThreadStopReason(lldb_private::Thread *thread)
{
    StopInfoSP stop_info_sp;
    return stop_info_sp;
}

lldb::ThreadSP
OperatingSystemGo::CreateThread(lldb::tid_t tid, addr_t context)
{
    Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_OS));

    if (log)
        log->Printf("OperatingSystemGo::CreateThread (tid = 0x%" PRIx64 ", context = 0x%" PRIx64 ") not implemented",
                    tid, context);

    return ThreadSP();
}

ValueObjectSP
OperatingSystemGo::FindGlobal(TargetSP target, const char *name)
{
    VariableList variable_list;
    const bool append = true;

    Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_OS));

    if (log)
    {
        log->Printf("exe: %s", target->GetExecutableModule()->GetSpecificationDescription().c_str());
        log->Printf("modules: %zu", target->GetImages().GetSize());
    }

    uint32_t match_count = target->GetImages().FindGlobalVariables(ConstString(name), append, 1, variable_list);
    if (match_count > 0)
    {
        ExecutionContextScope *exe_scope = target->GetProcessSP().get();
        if (exe_scope == NULL)
            exe_scope = target.get();
        return ValueObjectVariable::Create(exe_scope, variable_list.GetVariableAtIndex(0));
    }
    return ValueObjectSP();
}

TypeSP
OperatingSystemGo::FindType(TargetSP target_sp, const char *name)
{
    ConstString const_typename(name);
    SymbolContext sc;
    const bool exact_match = false;

    const ModuleList &module_list = target_sp->GetImages();
    size_t count = module_list.GetSize();
    for (size_t idx = 0; idx < count; idx++)
    {
        ModuleSP module_sp(module_list.GetModuleAtIndex(idx));
        if (module_sp)
        {
            TypeSP type_sp(module_sp->FindFirstType(sc, const_typename, exact_match));
            if (type_sp)
                return type_sp;
        }
    }
    Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_OS));

    if (log)
        log->Printf("OperatingSystemGo::FindType(%s): not found", name);
    return TypeSP();
}

OperatingSystemGo::Goroutine
OperatingSystemGo::CreateGoroutineAtIndex(uint64_t idx, Error &err)
{
    err.Clear();
    Goroutine result = {};
    ValueObjectSP g = m_allg_sp->GetSyntheticArrayMember(idx, true)->Dereference(err);
    if (err.Fail())
    {
        return result;
    }

    ConstString name("goid");
    ValueObjectSP val = g->GetChildMemberWithName(name, true);
    bool success = false;
    result.m_goid = val->GetValueAsUnsigned(0, &success);
    if (!success)
    {
        err.SetErrorToGenericError();
        err.SetErrorString("unable to read goid");
        return result;
    }
    name.SetCString("atomicstatus");
    val = g->GetChildMemberWithName(name, true);
    result.m_status = (uint32_t)val->GetValueAsUnsigned(0, &success);
    if (!success)
    {
        err.SetErrorToGenericError();
        err.SetErrorString("unable to read atomicstatus");
        return result;
    }
    name.SetCString("sched");
    val = g->GetChildMemberWithName(name, true);
    result.m_gobuf = val->GetAddressOf(false);
    name.SetCString("stack");
    val = g->GetChildMemberWithName(name, true);
    name.SetCString("lo");
    ValueObjectSP child = val->GetChildMemberWithName(name, true);
    result.m_lostack = child->GetValueAsUnsigned(0, &success);
    if (!success)
    {
        err.SetErrorToGenericError();
        err.SetErrorString("unable to read stack.lo");
        return result;
    }
    name.SetCString("hi");
    child = val->GetChildMemberWithName(name, true);
    result.m_histack = child->GetValueAsUnsigned(0, &success);
    if (!success)
    {
        err.SetErrorToGenericError();
        err.SetErrorString("unable to read stack.hi");
        return result;
    }
    return result;
}
